﻿using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;


namespace Ymatou.API.Common
{
    /// <summary>
    /// api执行方法
    /// </summary>
    public static class ApiClient
    {
        /// <summary>
        /// 执行请求
        /// </summary>
        /// <typeparam name="T">Response类型</typeparam>
        /// <param name="request">请求</param>
        /// <param name="serviceUrl">服务地址，某些服务默认可以不传，不确定时请联系服务提供方。通常格式为http://域名. 除普通格式外也支持复杂格式，如：query:http://api.query.ymatou.com;operate:http://api.operate.ymatou.com;default:http://api.default.ymatou.com 通常用来对应查询和操作分散在不同域名的情况</param>
        /// <param name="timeout">请求超时时间</param>
        /// <param name="proxy">设置代理</param>
        /// <param name="useNagleAlgorithm">是否启用Nagling Algorithm算法.该算法对于提交数据量小的Post请求可能会带来约200ms的延迟</param>
        public static BaseResponseModel<T> Execute<T>(this BaseRequestModel<T> request, string serviceUrl, int timeout, IWebProxy proxy, bool useNagleAlgorithm) where T : IResponse
        {
            return Execute(request, serviceUrl, timeout, proxy, useNagleAlgorithm, null);
        }

        /// <summary>
        /// 执行请求
        /// </summary>
        /// <typeparam name="T">Response类型</typeparam>
        /// <param name="request">请求</param>
        /// <param name="serviceUrl">服务地址，某些服务默认可以不传，不确定时请联系服务提供方。通常格式为http://域名. 除普通格式外也支持复杂格式，如：query:http://api.query.ymatou.com;operate:http://api.operate.ymatou.com;default:http://api.default.ymatou.com 通常用来对应查询和操作分散在不同域名的情况</param>
        /// <param name="timeout">请求超时时间</param>
        /// <param name="proxy">设置代理</param>
        /// <param name="useNagleAlgorithm">是否启用Nagling Algorithm算法.该算法对于提交数据量小的Post请求可能会带来约200ms的延迟</param>
        /// <param name="listener">侦听执行步骤</param>
        public static BaseResponseModel<T> Execute<T>(this BaseRequestModel<T> request, string serviceUrl = null, int timeout = 5000, IWebProxy proxy = null, bool useNagleAlgorithm = false, IExecutingListener listener = null) where T : IResponse
        {
            if (request == null)
            {
                throw new ArgumentNullException("request");
            }

            if (string.IsNullOrEmpty(request.ApiName))
            {
                throw new ArgumentException("request.ApiName不能为空");
            }

            serviceUrl = ServiceUrlHelper.GetServiceUrl(request, serviceUrl);
            if (string.IsNullOrWhiteSpace(serviceUrl))
            {
                throw new ArgumentException("服务地址无效");
            }

            //请求参数验证
            request.Validate();
            //
            var responseBody = Execute(
                request.ApiName,
                request.HttpGet ? JObject.FromObject(request) as object : JsonConvert.SerializeObject(request) as object,
                serviceUrl,
                request.HttpGet,
                timeout, proxy, useNagleAlgorithm
                );

            if (listener != null)
            {
                try
                {
                    listener.OnResponseBodyReceived(request, responseBody);
                }
                catch { }
            }

            return JsonConvert.DeserializeObject<BaseResponseModel<T>>(responseBody);
        }

        /// <param name="data">get时类型为JObject; post时类型为json字符串</param>
        /// <param name="realServiceUrl">服务的实际地址</param>
        internal static string Execute(string apiName, object data, string realServiceUrl, bool httpGet, int timeout = 5000, IWebProxy proxy = null, bool useNagleAlgorithm = false)
        {
            if (string.IsNullOrEmpty(apiName))
            {
                throw new ArgumentException("apiName不能为空");
            }

            if (string.IsNullOrWhiteSpace(realServiceUrl))
            {
                throw new ArgumentException("服务地址无效");
            }
            //兼容JAVA服务不支持域名后面双//杠的格式，如“http://operate.trading.iapi.ymatou.com//”
            realServiceUrl = realServiceUrl.TrimEnd('/');
            apiName = apiName.TrimStart('/');
            string requestUrl = string.Format("{0}/{1}", realServiceUrl, apiName);

            CurrentServiceUrl = requestUrl;

            if (httpGet)
            {
                return DoGet(requestUrl, data as JObject, timeout, proxy, useNagleAlgorithm);
            }

            return DoPost(requestUrl, data as string, timeout, proxy, useNagleAlgorithm);
        }

        public static string CurrentServiceUrl { get; private set; }

        #region inner

        /// <summary>
        /// 执行HTTP GET请求。
        /// </summary>
        /// <param name="url">请求地址</param>
        /// <param name="data">请求提交的参数</param>
        /// <returns>HTTP响应</returns>
        private static string DoGet(string url, JObject data, int timeout, IWebProxy proxy, bool useNagleAlgorithm)
        {
            if (url.Contains("?"))
            {
                url = url + "&" + BuildQuery(data);
            }
            else
            {
                url = url + "?" + BuildQuery(data);
            }
            HttpWebRequest req = GetWebRequest(url, "GET", timeout, proxy, useNagleAlgorithm);
            req.ContentType = "application/json;charset=utf-8";

            using (HttpWebResponse rsp = (HttpWebResponse)req.GetResponse())
            {
                Encoding encoding = Encoding.GetEncoding(rsp.CharacterSet);
                return GetResponseAsString(rsp, encoding);
            }
        }

        /// <summary>
        /// 组装普通文本请求参数。
        /// </summary>
        /// <param name="request">Key-Value形式请求参数字典</param>
        /// <returns>URL编码后的请求数据</returns>
        private static string BuildQuery(JObject data)
        {
            StringBuilder postData = new StringBuilder();
            bool hasParam = false;
            foreach (var keyPair in data.Properties())
            {
                // 忽略参数名或参数值为空的参数
                if (!String.IsNullOrEmpty(keyPair.Name))
                {
                    if (hasParam)
                    {
                        postData.Append("&");
                    }
                    postData.Append(keyPair.Name);
                    postData.Append("=");
                    postData.Append(keyPair.Value.ToString());
                    hasParam = true;
                }
            }
            return postData.ToString();
        }

        /// <summary>
        /// 执行HTTP POST请求。
        /// </summary>
        /// <param name="url">请求地址</param>
        /// <param name="jsonPostData">请求参数</param>
        /// <returns>HTTP响应</returns>
        private static string DoPost(string url, string jsonPostData, int timeout, IWebProxy proxy, bool useNagleAlgorithm)
        {
            HttpWebRequest req = GetWebRequest(url, "POST", timeout, proxy, useNagleAlgorithm);
            req.ContentType = "application/json;charset=utf-8";
            Byte[] postData = Encoding.UTF8.GetBytes(jsonPostData);
            Stream reqStream = req.GetRequestStream();
            reqStream.Write(postData, 0, postData.Length);
            reqStream.Close();

            using (HttpWebResponse rsp = (HttpWebResponse)req.GetResponse())
            {
                Encoding encoding = Encoding.GetEncoding(rsp.CharacterSet);
                return GetResponseAsString(rsp, encoding);
            }
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="url"></param>
        /// <param name="method"></param>
        /// <param name="Timeout"></param>
        /// <returns></returns>
        private static HttpWebRequest GetWebRequest(string url, string method, int timeout, IWebProxy proxy, bool useNagleAlgorithm)
        {
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(url);
            req.ServicePoint.Expect100Continue = false;
            req.ServicePoint.UseNagleAlgorithm = useNagleAlgorithm;
            req.Proxy = proxy;
            req.Method = method;
            req.KeepAlive = true;
            req.UserAgent = "YmatouAPI.V1.0.0.0";
            req.Accept = "application/json";
            req.Timeout = timeout;
            return req;
        }

        /// <summary>
        /// 把响应流转换为文本。
        /// </summary>
        /// <param name="rsp">响应流对象</param>
        /// <param name="encoding">编码方式</param>
        /// <returns>响应文本</returns>
        private static string GetResponseAsString(HttpWebResponse rsp, Encoding encoding)
        {
            #region 判断是否要处理 Transfer-Encoding: Chunked 不定长传输 
            bool bChunked = false;
            foreach (var v in rsp.Headers.AllKeys)
            {
                if (v.Equals("Transfer-Encoding", StringComparison.OrdinalIgnoreCase))
                {
                    if (rsp.Headers[v].Equals("chunked", StringComparison.OrdinalIgnoreCase))
                    {
                        bChunked = true;
                    }
                    break;
                }
            }
            #endregion

            Stream stream = null;
            StreamReader reader = null;

            try
            {
                // 以字符流的方式读取HTTP响应
                stream = rsp.GetResponseStream();
                reader = new StreamReader(stream, encoding);

                //不定长传输 读取
                if (bChunked)
                {
                    var content = new StringBuilder();
                    while (!reader.EndOfStream)
                    {
                        content.Append((char)reader.Read());
                    }
                    return content.ToString();
                }

                //标准读取
                return reader.ReadToEnd();
            }
            finally
            {
                // 释放资源
                if (reader != null) reader.Close();
                if (stream != null) stream.Close();
                if (rsp != null) rsp.Close();
            }
        }


        private class ServiceUrlHelper
        {
            public static string GetServiceUrl(IRequest request, string url)
            {
                if (request is IQueryModel)
                {
                    return GetByKey(url, Key_Query) ?? request.ServiceUrl ?? GetByKey(url, Key_Default) ?? url;
                }
                else if (request is IOperateModel)
                {
                    return GetByKey(url, Key_Operate) ?? request.ServiceUrl ?? GetByKey(url, Key_Default) ?? url;
                }
                else
                {
                    string result = null;
                    if (url != null)
                    {
                        var s = url.Trim();
                        if (s.Length > 3)
                        {
                            if (s.Substring(0, 4).Equals("http", StringComparison.OrdinalIgnoreCase))
                            {
                                result = s;
                            }
                            else
                            {
                                result = GetByKey(url, Key_Default);
                            }
                        }
                    }

                    return result ?? request.ServiceUrl ?? url;
                }
            }

            private static string GetByKey(string url, string key)
            {
                if (string.IsNullOrWhiteSpace(url)) return null;

                Dictionary<string, string> t;
                if (!all.TryGetValue(url, out t))
                {
                    t = GetUrlPairs(url);
                    if (all.Count >= MaxCount)
                    {
                        lock (allLock)
                        {
                            if (all.Count >= MaxCount)
                            {
                                all = new Dictionary<string, Dictionary<string, string>>();
                            }
                        }
                    }

                    all[url] = t;
                }

                string result;
                return t != null && t.TryGetValue(key, out result) ? result : null;
            }

            private static Dictionary<string, string> GetUrlPairs(string items)
            {
                var urls = new Dictionary<string, string>();
                string key, url;
                foreach (var item in items.Split(ItemsDelimeter, StringSplitOptions.RemoveEmptyEntries))
                {
                    var i = item.IndexOf(KeyValueDelimeter);
                    if (i < 1) continue;

                    key = item.Substring(0, i).Trim().ToLowerInvariant();

                    i++;
                    if (item.Length <= i) continue;

                    url = item.Substring(i).Trim();
                    if (url.Length < 1) continue;

                    urls[key] = url;
                }

                return urls.Count < 1 ? null : urls;
            }

            private static readonly char[] ItemsDelimeter = new[] { ';' };
            private const char KeyValueDelimeter = ':';
            private const string Key_Query = "query";
            private const string Key_Operate = "operate";
            private const string Key_Default = "default";
            private const int MaxCount = 100;
            private static Dictionary<string, Dictionary<string, string>> all = new Dictionary<string, Dictionary<string, string>>();
            private static readonly object allLock = new object();
        }

        #endregion

        public interface IExecutingListener
        {
            void OnResponseBodyReceived(object request, string responseBody);
        }
    }
}
